#include <QDebug>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickView>
#include <QQuickWindow>
#include <QSGRendererInterface>

#include <ui/algorithmProcess/AlgorithmProcessor.h>
#include <ui/algorithmProcess/RayTracingProcessor.h>
#include <ui/scene/Scene.h>
#include <ui/tree/ParaTreeViewController.h>
#include <ui/tree/TopoTreeViewController.h>
#include <ui/window/dataVisualizationWindow/ColorMap.h>
#include <ui/window/dataVisualizationWindow/FluxMapParameter.h>
#include <ui/window/dataVisualizationWindow/ImageAxis.h>
#include <ui/window/dataVisualizationWindow/ImageColorBar.h>
#include <ui/window/dataVisualizationWindow/Map.h>
#include <ui/window/dataVisualizationWindow/SceneRendererAdapter.h>
#include <ui/window/simulationWindow/AnalyticFunctionWindowAdapter.h>
#include <ui/window/simulationWindow/RayTracingParameter.h>

#include <algorithm/analytical_model/algorithm/cuda/Utils.h>
#include <algorithm/analytical_model/algorithm/pipeline/AnalyticalPipeline.h>
#include <algorithm/analytical_model/algorithm/pipeline/AnalyticalPipelineParam.h>
#include <algorithm/analytical_model/algorithm/pipeline/DumpArgs.h>
#include <algorithm/analytical_model/algorithm/pipeline/Utils.h>
#include <algorithm/analytical_model/io/FileTypes.h>
#include <algorithm/analytical_model/io/bin/WriteBinary.h>
#include <algorithm/analytical_model/io/bin/WriteFluxMap.h>
#include <algorithm/rayTracing/rayTracingPipeline/RayTracingPipeline.h>
#include <utils/Format.h>

#ifdef _WIN32
    #include <Windows.h>
#endif

#include <chrono>
#include <iostream>
#include <memory>
#include <type_traits>
#include <vector>

#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/json.hpp>
#include <boost/program_options.hpp>
#include <boost/range.hpp>

#include <cuda_runtime.h>
#include <cuda_runtime_api.h>

static void myMessageHandle(QtMsgType msg_type, const QMessageLogContext& /*unused*/,
                            const QString& msg)
{
    switch (msg_type)
    {
    case QtMsgType::QtDebugMsg:
        std::cout << solar::format("[DEBUG] {}\n", msg.toStdString());
        break;
    case QtMsgType::QtCriticalMsg:
        std::cout << solar::format("[CRITICAL] {}\n", msg.toStdString());
        break;
    case QtMsgType::QtFatalMsg:
        std::cout << solar::format("[FATAL] {}\n", msg.toStdString());
        break;
    case QtMsgType::QtInfoMsg:
        std::cout << solar::format("[INFO] {}\n", msg.toStdString());
        break;
    case QtMsgType::QtWarningMsg:
        std::cout << solar::format("[WARNING] {}\n", msg.toStdString());
        break;
    default:
        std::cout << solar::format("{}\n", msg.toStdString());
        break;
    }
}

static void printHelpMessage(const std::string_view& program_name)
{
    std::cout << solar::format(
        "Solar Tower Simulation System by SolarEnergy@ZJU CAD&CG\nUsage: {} [options] <input>\n",
        program_name);
}

auto main(int argc, char* argv[]) -> int
{
#ifdef _WIN32
    // Windows的GUI窗口没有stdio，看不见printf/std::cout
    // solar::println封装了QDebug()，但该方法无法在CUDA Kernel中调用（host函数）
    // 使用Windows API创建console
    if (AttachConsole(ATTACH_PARENT_PROCESS) == FALSE)
    {
        AllocConsole();
    }
    freopen("CONIN$", "r", stdin);
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
#endif

    boost::program_options::options_description desc{"Options"};
    boost::program_options::positional_options_description positional_desc;
    desc.add_options()("help,h", "Help screen")("cli", "Use command-line interface (disable GUI)")(
        "dump-args", "Dump args related to the model, instead of running the simulation pipeline")(
        "inputs", boost::program_options::value<std::vector<std::string>>(),
        "Inputs, currently supports: [json]")(
        "outputs,o", boost::program_options::value<std::vector<std::string>>(),
        "Output file names")("output-types",
                             boost::program_options::value<std::vector<std::string>>(),
                             "Output file types, auto-detect if you don't use this option, "
                             "currently supports: [csv, bin] for fluxmap, [json] for dump args")(
        "repeat-times", boost::program_options::value<std::size_t>()->default_value(1ULL),
        "Run ray-tracing for N times to calculate the average result (N >= 1)")(
        "image-plane", "Convert rectangle receiver to image-plane (internal use only)");
    positional_desc.add("inputs", -1);
    boost::program_options::command_line_parser command_line_parser{argc, argv};
    command_line_parser.options(desc)
        .style(boost::program_options::command_line_style::default_style |
               boost::program_options::command_line_style::allow_slash_for_short |
               boost::program_options::command_line_style::case_insensitive)
        .positional(positional_desc);
    boost::program_options::variables_map vm;
    try
    {
        boost::program_options::parsed_options parsed_options = command_line_parser.run();
        boost::program_options::store(parsed_options, vm);
        boost::program_options::notify(vm);
    }
    catch (const boost::program_options::error& e)
    {
        std::cerr << e.what() << '\n';
        return -1;
    }

    if (vm.count("help") > 0)
    {
        printHelpMessage(argv[0]);
        std::cout << "\n\n" << desc << '\n';
        return 1;
    }

    // auto params = solar::loadAnalyticalPipelineParam(
    //     solar::io::JSONType{},
    //     std::string_view(R"(D:\Learn\solar_software\solar_3\test_10x.json)"));
    // params.simulation_option_ = solar::SimulationOption::PerHeliostat;
    // auto start = std::chrono::system_clock::now();
    // solar::runAnalyticalModelPipeline(std::addressof(params));
    // auto end = std::chrono::system_clock::now();
    // auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    // std::cout << double(duration.count()) * std::chrono::microseconds::period::num /
    //                  std::chrono::microseconds::period::den
    //           << '\n';
    // solar::testUNIZAR();
    // cudaDeviceReset();

    if (vm.count("cli") == 0)
    {
        QGuiApplication app(argc, argv);

        QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);

        QQmlApplicationEngine engine;
        const QUrl url(u"qrc:/solar_2/zqml/Main.qml"_qs);
        QObject::connect(
            &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
            []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);

        /*
            //---------------测试fun1.h------------------
            fun1 test1;
            double ret1 = fun1::cal_sqrt(8);
            qDebug() <<"==========================";
            qDebug() <<"fun1 test ret1 = "<<ret1 ;
            //----------------------------------------

            //---------------测试fun2.h------------------
            fun2 test2;
            double ret2 = test2.times_ten_to_sqrt(4.0);
           test2.useglobal();
            qDebug() <<"==========================";
            test2.setNumber(5);
            qDebug() <<" number = "<<test2.getNumber();
            qDebug() <<"fun2 test ret2 = "<<ret2 ;
            //----------------------------------------
        */

        // 设置上下文环境
        // solar::Scene scene; // 要给scene类设置Q_OBJECT

        std::shared_ptr<solar::Scene> scene = std::make_shared<solar::Scene>();
        qDebug() << "scene finished";
        engine.rootContext()->setContextProperty(
            "topoControl", scene->topo_tree_control_.get()); // 这里传入的应该是普通指针
        engine.rootContext()->setContextProperty("paraControl", scene->para_tree_control_.get());

        // std::shared_ptr<ColorMap> colormap = std::make_shared<ColorMap>();

        // raytracing
        std::shared_ptr<solar::RayTracingParameter> raytracing_parameter =
            std::make_shared<solar::RayTracingParameter>();
        qDebug() << "raytracing finished";
        engine.rootContext()->setContextProperty("rayTracingParameter", raytracing_parameter.get());

        // analytical
        solar::AnalyticFunctionWindowAdapter analytic_function_window_adapter;
        engine.rootContext()->setContextProperty("analyticFunctionWindowAdapter",
                                                 std::addressof(analytic_function_window_adapter));
        qmlRegisterUncreatableType<solar::AnalyticFunctionWindowEnums>(
            "AnalyticFunctionEnums", 1, 0, "AnalyticFunctionEnums",
            "Not creatable as it is an enum type");

        // flux map plotting
        std::shared_ptr<solar::FluxMapParameter> fluxmap_parameter =
            std::make_shared<solar::FluxMapParameter>();
        engine.rootContext()->setContextProperty("fluxMapParameter", fluxmap_parameter.get());

        // std::shared_ptr<RayTracingProcessor> raytracing_processor =
        // std::make_shared<RayTracingProcessor>();
        // engine.rootContext()->setContextProperty("rayTracingProcessor",
        // raytracing_processor.get());

        // 算法
        std::shared_ptr<solar::AlgorithmProcessor> algorithm_processor =
            std::make_shared<solar::AlgorithmProcessor>(scene, raytracing_parameter,
                                                        fluxmap_parameter, std::addressof(engine));
        engine.rootContext()->setContextProperty("algorithmProcessor", algorithm_processor.get());

        // qmlRegisterType<ColorMap>("custom.colormap",1,0,"ColorMap");

        qmlRegisterType<solar::Map>("OpenGLMap", 1, 0, "Map");
        qmlRegisterType<solar::SceneRendererAdapter>("SceneRendererAdapter", 1, 0,
                                                     "SceneRendererAdapter");

        // 轴及colorbar
        qmlRegisterType<solar::ImageAxis>("ImageAxis", 1, 0, "AutoAxis");
        qmlRegisterType<solar::ImageColorBar>("ImageColorBar", 1, 0, "AutoColorBar");

        engine.load(url);

        // QQuickView view;
        // view.setResizeMode(QQuickView::SizeRootObjectToView);
        // view.setSource(url);
        // view.show();

        return QGuiApplication::exec();
    }

    qInstallMessageHandler(myMessageHandle);
    if (vm.count("inputs") > 0)
    {
        auto inputs = vm["inputs"].as<std::vector<std::string>>();
        auto outputs = vm["outputs"].as<std::vector<std::string>>();
        std::vector<std::string> output_types;
        if (vm.count("output-types") > 0)
        {
            output_types = vm["output-types"].as<std::vector<std::string>>();
        }
        bool auto_detect_output_types = output_types.empty();
        if (output_types.size() == 1)
        {
            output_types.insert(output_types.begin() + 1, outputs.size() - 1, output_types[0]);
        }
        if (inputs.size() != outputs.size())
        {
            std::cout << "[ERROR] The count of inputs and outputs should be the same\n";
        }
        if (output_types.size() != outputs.size() && !auto_detect_output_types)
        {
            std::cout
                << R"([ERROR] The count of args of "output-types" and "outputs" should be the same, or provide only one arg to "output-types" in order to set all outputs' type)"
                << '\n';
        }

        solar::RayTracingPipeline ray_tracing_pipeline;
        for (auto i = 0ULL; i < inputs.size(); i++)
        {
            const auto& input = inputs[i];
            const auto& output = outputs[i];
            if (!auto_detect_output_types)
            {
                const auto& output_type = output_types[i];
            }
            boost::iostreams::mapped_file_source input_buffer(input.data());
            input_buffer.close();
            input_buffer.open(input.data());
            boost::json::stream_parser json_parser;
            json_parser.reset();
            json_parser.write(input_buffer.data(), input_buffer.size());
            json_parser.finish();
            auto json_value = json_parser.release();
            input_buffer.close();
            const auto* value_ptr = json_value.if_object();
            if (value_ptr == nullptr)
            {
                continue;
            }
            if (const auto* algorithm_value = value_ptr->if_contains("algorithm");
                algorithm_value != nullptr)
            {
                cudaStream_t stream = nullptr;
                cudaStreamCreate(std::addressof(stream));
                if (algorithm_value->get_string() == "analytical")
                {
                    auto params =
                        solar::loadAnalyticalPipelineParam(solar::io::JSONType{}, json_value);
                    params.simulation_option_ = solar::SimulationOption::PerHeliostat;
                    if (vm.count("dump-args") == 0)
                    {
                        const auto& [d_flux_data, resolution] =
                            solar::runAnalyticalModelPipeline(std::addressof(params), stream);
                        std::cout << solar::format("{}, {}\n", resolution.x, resolution.y);
                        // std::cout << solar::totalEnergyOnFluxMap({resolution.x, resolution.y},
                        //                                          d_flux_data)
                        //           << '\n';
                        cudaStreamSynchronize(stream);
                        solar::io::bin::writeFluxMap(output, d_flux_data, resolution.y,
                                                     resolution.x);
                    }
                    else
                    {
                        const auto json_string = boost::json::serialize(
                            solar::dumpAnalyticalModelArgs(std::addressof(params)));
                        solar::io::bin::writeBinary(output, json_string.c_str(),
                                                    sizeof(char) * json_string.size());
                    }
                }
                else
                {
                    const auto repeat_times = vm["repeat-times"].as<std::size_t>();
                    float* d_avg_result = nullptr;
                    float* d_flux_data = nullptr;
                    int2 resolution{};
                    std::vector<float*> results(repeat_times);
                    if (vm.count("image-plane") > 0)
                    {
                        std::tie(d_flux_data, resolution) =
                            ray_tracing_pipeline.rayTracingOnlyImagePlane(true, input, stream);
                    }
                    else
                    {
                        std::tie(d_flux_data, resolution) =
                            ray_tracing_pipeline.rayTracingOnly(true, input, stream);
                    }
                    results[0] = d_flux_data;
                    CHECK_CUDA_ERRORS(cudaMallocAsync(std::addressof(d_avg_result),
                                                      sizeof(float) * resolution.x * resolution.y,
                                                      stream));
                    if (vm.count("image-plane") > 0)
                    {
                        for (std::size_t i = 1; i < repeat_times; i++)
                        {
                            results[i] = std::get<0>(ray_tracing_pipeline.rayTracingOnlyImagePlane(
                                false, input, stream));
                        }
                    }
                    else
                    {
                        for (std::size_t i = 1; i < repeat_times; i++)
                        {
                            results[i] = std::get<0>(
                                ray_tracing_pipeline.rayTracingOnly(false, input, stream));
                        }
                    }

                    solar::avgFluxMaps({resolution.x, resolution.y}, d_avg_result, results,
                                       repeat_times, stream);
                    cudaStreamSynchronize(stream);
                    std::cout << solar::format("{}, {}\n", resolution.x, resolution.y);
                    // std::cout << solar::totalEnergyOnFluxMap({resolution.x, resolution.y},
                    //                                          d_flux_data)
                    // << '\n';
                    solar::io::bin::writeFluxMap(output, d_avg_result, resolution.y, resolution.x);
                }
            }
        }
    }

    return 0;
}
